package model;

import java.util.*;

/**
 * @author  sangwo.y
 */
public class Game extends Observable {
	
	/**
	 * @author  sangwo.y
	 */
	public enum Cell {FREE_CELL_ONE, FREE_CELL_TWO, FREE_CELL_THREE, FREE_CELL_FOUR; }
	
	private final static int FREE_CELLS = 4;
	private final static int LAST_OUT = 4; 
	private final static int PILES = 8;
	private final static int CARDS_IN_DECK = 52; 

	
	private List<Card> theDeck;
	/**
	 * @uml.property  name="freeCells"
	 * @uml.associationEnd  multiplicity="(0 -1)"
	 */
	private Card[] freeCells = {null, null, null, null};
	//private Set<Card> playedCards;
	/**
	 * @uml.property  name="lastOut"
	 * @uml.associationEnd  qualifier="key:java.lang.Object pro.model.core.Card"
	 */
	private Map<Card.Suit,Card> lastOut;
	/**
	 * @uml.property  name="piles"
	 * @uml.associationEnd  multiplicity="(0 -1)"
	 */
	private Pile[] piles;
	private List<Card> inPlay;
	private int inPlaySrc;
	
	private int score;

	
	public Game() {
		initAll();
		autoDealHands();
	}
	
	/**
	 * @return  the score
	 * @uml.property  name="score"
	 */
	public int getScore() {
		return CARDS_IN_DECK -score;
	}
	/**
	 * @return  the piles
	 * @uml.property  name="piles"
	 */
	public Pile[] getPiles() {
		return piles;
	}
	
	/**
	 * @return  the freeCells
	 * @uml.property  name="freeCells"
	 */
	public Card[] getFreeCells() {
		return freeCells;
	}
	
	public Map<Card.Suit,Card> getLastCardsOut() {
		return lastOut;
	}
	
	public List<Card> getCurrentCard()	{
		return inPlay;
	}
	

	private void autoDealHands() {
		newDeck(); shuffleDeck(); dealHands();
	}
	
	public void dealHands(List<Card> deck) {
		theDeck=deck;
		dealHands();
	}
	
	
	private void initAll() {
		theDeck = new ArrayList<Card>(CARDS_IN_DECK);		
		//freeCells = new Card[FREE_CELLS];
		//playedCards = new HashSet<Card>();
		lastOut = new HashMap<Card.Suit,Card>(LAST_OUT);
		lastOut.put(Card.Suit.Spade, null);
		lastOut.put(Card.Suit.Diamond, null);
		lastOut.put(Card.Suit.Heart, null);
		lastOut.put(Card.Suit.Club, null);
		inPlay = new ArrayList<Card>();
		score=0;
		newPiles(PILES); 	
	}
	
	
	public boolean moveToPlayedCards() throws MoveToPlayedCardsException {
		if (inPlay.size()==1) {
			Card c = inPlay.get(0);
			Card last = lastOut.get(c.getSuit());
			if (last==null) {
				if (c.getValue().equals(Card.Value.Ace)) {
					lastOut.put(c.getSuit(), c);
					inPlay.clear();
					inPlaySrc=0;
					score++;
					signal(c,c.getSuit());
					return true;
				}
				System.out.println("First Card Out Must Be Ace.");
				restoreMove();
				return false;
			} else if (last.isSameNext(c)) {
				lastOut.put(c.getSuit(), c);
				inPlay.clear();
				inPlaySrc=0;
				score++;
				signal(c,c.getSuit());
				return true;
			}			
			System.out.println("Card Out Must Follow The Last One Out.");
			restoreMove();
			return false;			
		} else {
			restoreMove();
			System.out.println("No or Too Many Played Cards.");
			return false; 
		}
			
		
	}
	
	
	public List<Card> moveFromFreeCell(Cell cell) throws MoveFromFreeCellException {
		if (inPlay.isEmpty()) {
			System.out.println("Asked: "+(cell.ordinal()+11));
			if (freeCells[cell.ordinal()]!=null) {
				inPlay.add(freeCells[cell.ordinal()]);
				freeCells[cell.ordinal()]=null;
				inPlaySrc=cell.ordinal()+10+1; //+const				
			} else { throw new MoveFromFreeCellException("Empty Cell. Invalid Argument."); }
			return inPlay;
			
		} else {throw new MoveFromFreeCellException("Make Previous Move First."); }
		
	}
	
	public boolean moveToFreeCell(Cell cell) throws MoveToFreeCellException {
		//System.out.println(inPlay.size());
		//System.out.println(freeCells[cell.ordinal()]);
		if (	(inPlay.size()==1)	&&	(freeCells[cell.ordinal()]==null)	) {
			System.out.println("Moving to: "+(cell.ordinal()+11)); 
			freeCells[cell.ordinal()]=inPlay.get(0);	
			signal(inPlay.get(0),cell);
			inPlay.clear();
			inPlaySrc=0;			
			return true;
		} else {
			if (inPlay.size()<1)
				throw new MoveToFreeCellException("No Card Selected.");
			restoreMove();
			System.out.println("FreeCell Occupied or Too Many Cards Selected.");
			return false;
		}
	}
	
	
	public boolean moveToPile(int dst) throws MoveToPileException {
		if (!inPlay.isEmpty()) {
			System.out.println("Moving to: "+dst);
			if (!checkPileId(dst)) 
				throw new MoveToPileException("Invalid Column Call Parameter. ");
			//System.out.println();
			Pile thePile = piles[dst-1];
			if (thePile.isEmpty()	&&	(inPlay.size()>maxCardsAllowedToBeMoved()-1)) {
				System.out.println("Acces denied... Pushing internally back.");
				restoreMove();
				return false;
			}
			if (thePile.push(inPlay)) {
				signal(inPlay.get(0),thePile);
				inPlaySrc=0;
				inPlay.clear();				
				return true; 
			}			
		} else { throw new MoveToPileException("Make A Move First. ");
			//System.out.println("Must make a move first.");	
				//return false;
		}
		System.out.println("Acces denied... Pushing internally back.");
		restoreMove();
		return false;
	}
	
	private void restoreMove() {
		if (inPlaySrc<9) {
			piles[inPlaySrc-1].pushInternal(inPlay);
			inPlaySrc=0;			
			inPlay.clear();
		}	
		if (	(inPlaySrc>10)	&&	(inPlaySrc<15)	) {
			freeCells[inPlaySrc-11]=inPlay.get(0);
			inPlaySrc=0;
			inPlay.clear();
		}
	}
	
	private boolean checkPileId(int id) {
		if (	(id<1)	||	(id>PILES)	) {
			return false;
		}
		return true;
	}
	
	public List<Card> moveFromPile(int src, int n) throws MoveFromPileException {
		if (inPlay.isEmpty()) {
			System.out.println("Asked: "+n+" at "+src);
			if (!checkPileId(src)) 
				throw new MoveFromPileException("Invalid Column Call Parameter. ");
			Pile thePile = piles[src-1];
			if (	(n>thePile.size())	|| (n<=0) || (n>maxCardsAllowedToBeMoved())	) 
				throw new MoveFromPileException("Invalid Call Parameters or No / Too Much Cards to Move.");
			

			//if (	!thePile.empty()	&&	!(freeCellsCount()<n-1)	) { 
				List<Card> tmpList = thePile.pop(n);
				if (tmpList!=null) {
					inPlaySrc=src;
					inPlay.addAll(tmpList);
				} else {throw new MoveFromPileException("Can Not Pop. Probably No Valid Sequence."); }
				return inPlay;		
			//}
			//throw new MoveFromPileException("No or too much cards to move. ");
		} else {throw new MoveFromPileException("Must Play The Previous Move First."); }
	}
	
	private int maxCardsAllowedToBeMoved() {
		int freeSpots = freeCellsCount();
		int pileSpots = emptyPiles();
		int plus = 1;
		
//		if ((freeSpots == 0)&&(pileSpots!=0)) plus=0;
			
		return freeSpots+plus+pileSpots;
	}
	
	private int emptyPiles() {
		
		int count = 0;
		for (int i=0; i<PILES; i++) {
			if (piles[i].isEmpty()) 
				count++;
		}
		System.out.println("empty piles:"+count);
		return count;
	}

	
	private int freeCellsCount() {
		int count = 0;
		for (int i=0; i<FREE_CELLS; i++) {
			if (freeCells[i]==null) count++;		
		}
		return count;
	}
	
	
	private void dealHands() {
		int count = 0;
		for (int i=0; i<4; i++) {
			for (int j=0; j<7; j++) {
				Card card = theDeck.get(count);
				piles[i].pushInternal(card);
				count++;
			}
		}
		for (int i=4; i<8; i++) {
			for (int j=0; j<6; j++) {
				Card card = theDeck.get(count);
				piles[i].pushInternal(card);
				count++;
			}
		}
	}
	
	private void newPiles(int n) {
		piles = new Pile[n];
		for (int i=0; i<n; i++) {
			piles[i] = new Pile(i+1);
		}
	}
	
	private void newDeck() {
		Card.Value[] cv = Card.Value.values();
		Card.Suit[] cs = Card.Suit.values();
		for (int i=0; i<cv.length; i++) {
			for (int j=0; j<cs.length; j++) {
				Card card = new Card(cs[j], cv[i]);
				//System.out.println(card);
				theDeck.add(card);
				
			}			
		}
	}
	
	private void shuffleDeck() {
		Random rnd = new Random();
		for (int i=theDeck.size(); i>1; i--) {
			swap(i-1, rnd.nextInt(i));
		}
	}
	
	private void swap(int idx1, int idx2) {
		theDeck.set(idx1, theDeck.set(idx2, theDeck.get(idx1)));
	}
	
	public String toString() {
		StringBuffer sb = new StringBuffer();
		for (int i=0; i<8; i++) {			
			sb.append(piles[i].toString()); sb.append("\n");
		}
		sb.append("FreeCells: "); sb.append(freeCells[0]); sb.append(" | "); sb.append(freeCells[1]);
		sb.append(" | "); sb.append(freeCells[2]); sb.append(" | "); sb.append(freeCells[3]); sb.append("\n");
		sb.append("OutOfPlay: "); sb.append(lastOut.get(Card.Suit.Spade)); sb.append(" | "); 
		sb.append(lastOut.get(Card.Suit.Diamond));
		sb.append(" | "); sb.append(lastOut.get(Card.Suit.Heart)); sb.append(" | "); 
		sb.append(lastOut.get(Card.Suit.Club)); sb.append("\n");
		//sb.append("CurInPlay: "); sb.append(inPlay.toString()); sb.append(" from "); sb.append(inPlaySrc); sb.append("\n");
		return sb.toString(); 
		
	}
	
	
	@SuppressWarnings("serial")
	public class MoveFromPileException extends Exception {
		MoveFromPileException(String msg) {
			super(msg);
		}
	}
	
	@SuppressWarnings("serial")
	public class MoveToPileException extends Exception {
		MoveToPileException(String msg) {
			super(msg);
		}
	}
	
	@SuppressWarnings("serial")
	public class MoveToFreeCellException extends Exception {
		MoveToFreeCellException(String msg) {
			super(msg);
		}
	}
	
	@SuppressWarnings("serial")
	public class MoveFromFreeCellException extends Exception {
		MoveFromFreeCellException(String msg) {
			super(msg);
		}
	}
	
	@SuppressWarnings("serial")
	public class MoveToPlayedCardsException extends Exception {
		MoveToPlayedCardsException(String msg) {
			super(msg);
		}
	}
	
	private void signal(Card card, Object destination) {	
		setChanged();
		Object[] o = {card, destination};
		notifyObservers(o);
	}
	
}
